/*
 * Drag and drop helpers, provide basic drag and drop events notification and 
 * a simple width/height resize function together with yfit.js.
 * 
 * See test_ydnd.html for detailed usage.
 *
 * @author Li Yang (young4chong@gmail.com)
 * @dependency none; OPTIONAL: yfit.js, yevent.js
 */

var Ydnd = (function() {
	var self = {}; self.prototype = {}
	
	var isIE = document.all && !/Opera/i.test(navigator.userAgent);
	
	// style - camelized style name, like 'paddingTop'
	function _getStyle(elem, style) {
		var value = elem.style[style];
		if (!value || value == 'auto') {
			if (elem.currentStyle) {
				value = elem.currentStyle[style];
			} else if (elem.ownerDocument.defaultView) {
				var css = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
				value = css ? css[style] : null;
			}
		}
		return value;
	}
	
	function _pos(elem) {
		var x = parseInt(elem.style.left);
		var y = parseInt(elem.style.top);
		return [isNaN(x)?0:x, isNaN(y)?0:y];
	}

	function _abspos(elem) {
		var valueT = 0, valueL = 0;
		
		var node = elem;
		do {
			valueT += node.offsetTop  || 0;
			valueL += node.offsetLeft || 0;
		} while (node = node.offsetParent);
		
		node = elem;
		while (node = node.parentNode) {
			valueT -= node.scrollTop  || 0;
			valueL -= node.scrollLeft || 0;
		}
		
		return [valueL, valueT];
	}

	function _winSize() {
		var winWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
		var winHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
		return [winWidth, winHeight];
	}
	
	var BIG_Z_INDEX = 10000;
	var THRESHOLD = 5; // value 7+ had problem in IE7, failed to start draggingg
	var holding = dragging = selected = null;
	var winSize;
	
	/*
	 * Start dragging with the given mousedown event and the drag element. If drag element
	 * is omitted, the event's source element is used.
	 */
	function startDragging(event, dragElem, allSelected) {
		if (holding != null)
			return; // already draggingg something...

		event = event || window.event;
		if (!event || event.type != 'mousedown')
			return; // invalid call, can only start with mousedown event
		
		var e = dragElem || (event.target || event.srcElement);
		var s = allSelected || [e];
		if (e.oMouseDown) e.oMouseDown(event);
		
		// remember element's initial status
		for (var i = 0, f; i < s.length; i++) {
			f = s[i];
			f.oOffset = [event.clientX, event.clientY];
			f.dOffset = [event.clientX, event.clientY];
			f.oOpacity = _getStyle(f, 'opacity');
			f.oZIndex = _getStyle(f, 'zIndex');
			f.oOverflow = _getStyle(f, 'overflow');
			var pos = _getStyle(f, 'position');
			if (pos!='absolute' && pos!='relative')
				{ alert('To be draggable, position must be absolute or relative'); return; }
		}

		holding = e;
		selected = s;
		winSize = _winSize();
		document.onmousemove = onMouseMove;
		document.onmouseup = onMouseUp;
		
		// cancel annoying text selection
		if (isIE) e.onselectstart = function() {return false;}
		if (typeof(e.style.MozUserSelect)!='undefined') e.style.MozUserSelect='none';
		return false; // cancel selection behavior
	}

	function onMouseMove(event) {
		var e = holding;
		var s = selected;
		event = event || window.event;
		var x = event.clientX, y = event.clientY;

		if (x < 0 || y < 0 || x >= winSize[0] || y >= winSize[1])
			return false;

		// drag started ?
		if (!dragging) {
			if (Math.abs(x-e.oOffset[0])+Math.abs(y-e.oOffset[1]) < THRESHOLD)
				return false;
			// drag start
			dragging = e;
			for (var i = 0, f; i < s.length; i++) {
				f = s[i];
				f.style.zIndex = BIG_Z_INDEX;
				f.style.opacity = 0.75;
				f.style.overflow = 'hidden'; // work around the flashing while draggingg in FF
			}
			_notifyDragStart(e, s, event);
		}
		
		// move e to the new position
		var p = _pos(e);
		var dx = x - e.dOffset[0], dy = y - e.dOffset[1];
		e.dOffset[0] = event.clientX; e.dOffset[1] = event.clientY; 
		if (dx==0 && dy==0) return false;
		
		// gives a chance to adjust the new position
		var nx = p[0] + dx, ny = p[1] + dy;
		var np = _notifyBeforeDragMove(e, [nx, ny], [dx, dy], p, event);
		if (np) {
			// move to new position
			e.style.left = np[0]+'px'; e.style.top = np[1]+'px';
			dx = np[0] - p[0]; dy = np[1] - p[1];
		} else {
			// don't move
			dx = dy = 0;
		}

		// notify drag move, and ENSURE pos before and after is the same for possible DOM change
		var absPosBefore = _abspos(e);
		_notifyDragMove(e, s, np, [dx, dy], event);
		var absPosAfter = _abspos(e);
		dx = absPosBefore[0] - absPosAfter[0];
		dy = absPosBefore[1] - absPosAfter[1];
		if (dx != 0 || dy != 0) {
			p = _pos(e);
			np = [p[0] + dx, p[1] + dy];
			e.style.left = np[0]+'px'; e.style.top = np[1]+'px';
		}
		
		// all selected follow e's movement
		if (np) {
			for (var i = 0, f; i < s.length; i++) {
				f = s[i];
				if (f == e) continue;
				f.style.left = np[0]+'px'; f.style.top = np[1]+'px';
			}
		}
		return false; // cancel selection behavior
	}
	
	function onMouseUp(event) {
		event = event || window.event;
		var e = dragging;
		var s = selected;

		// restore annoying text selection
		if (isIE) holding.onselectstart = null;
		if (typeof(holding.style.MozUserSelect)!='undefined') holding.style.MozUserSelect='';
		
		document.onmousemove = document.onmouseup = null;
		holding = dragging = selected = null;
		
		if (e) {
			for (var i = 0, f; i < s.length; i++) {
				f = s[i];
				f.style.opacity = f.oOpacity;
				f.style.zIndex = f.oZIndex;
				f.style.overflow = f.oOverflow;
			}
			// cancel the click event caused by drag & drop
			e.mouseUpTime = new Date().getTime();
			if (e.onclick != _cancelOneClick) {
				e.oOnClick = e.onclick;
				e.onclick = _cancelOneClick;
			}
			_notifyDragEnd(e, s, event);
		}
		
		return false;
	}
	
	function _cancelOneClick(evt) {
		evt = evt || window.event;
		this.onclick = this.oOnClick;
		this.oOnClick = null;
		if (new Date().getTime() - this.mouseUpTime < 600) {
			// cancel the click triggered by drag & drop
			if (evt.preventDefault) {
				evt.preventDefault();
				evt.stopPropagation();
			} else {
				evt.returnValue = false;
				evt.cancelBubble = true;
			}
			return false;
		} else {
			if (this.onclick)
				this.onclick(evt);
		}
	}
	
	function _notifyDragStart(e, selected, event) {
		if (e.onDragStart) e.onDragStart(selected, event);
		var p = e.parentNode;
		if (p.onChildDragStart) p.onChildDragStart(e, selected, event);
	}
	
	function _notifyBeforeDragMove(e, newPos, delta, oldPos, event) {
		if (e.beforeDragMove)
			return e.beforeDragMove(newPos, delta, oldPos, event);
		var p = e.parentNode;
		if (p.beforeChildDragMove)
			return p.beforeChildDragMove(e, newPos, delta, oldPos, event);
		return newPos;
	}
	
	function _notifyDragMove(e, selected, newPos, delta, event) {
		if (e.onDragMove) e.onDragMove(newPos, delta, event);
		var p = e.parentNode;
		if (p.onChildDragMove) p.onChildDragMove(e, selected, newPos, delta, event);
	}
	
	function _notifyDragEnd(e, selected, event) {
		if (e.onDragEnd) e.onDragEnd(event);
		var p = e.parentNode;
		if (p.onChildDragEnd) p.onChildDragEnd(e, selected, event);
	}
	
	function startResizingHeight(event, handler, side1, side2) {
		return _startResizing(event, handler, side1, side2, 0);
	}
	
	function startResizingWidth(event, handler, side1, side2) {
		return _startResizing(event, handler, side1, side2, 1);
	}
	
	function _startResizing(event, handler, side1, side2, horw) {
		if (!side1 || !side2) {
			for (side1=handler.previousSibling; side1 && side1.nodeType!=1; side1=side1.previousSibling) ;
			for (side2=handler.nextSibling; side2 && side2.nodeType!=1; side2=side2.nextSibling) ;
		}
		if (!side1 || !side2) return;
		
		var p = side1.parentNode;
		var size1 = horw ? Yfit.getPureWidth(side1) : Yfit.getPureHeight(side1);
		var size2 = horw ? Yfit.getPureWidth(side2) : Yfit.getPureHeight(side2);
		var min1 = parseInt(side1.getAttribute(horw ? 'minWidth' : 'minHeight') || '0');
		var min2 = parseInt(side2.getAttribute(horw ? 'minWidth' : 'minHeight') || '0');
		
		event = event || window.event;
		var base = horw ? event.clientX : event.clientY, change;
		
		handler.beforeDragMove = function(newPos, delta, oldPos, event) {
			var now = horw ? event.clientX : event.clientY, change;
			if (now < base) {
				change = Math.min(base - now, Math.max(size1-min1, 0));
				side1.style[horw?'width':'height'] = (size1-change)+'px';
				side2.style[horw?'width':'height'] = (size2+change)+'px';
			} else {
				change = Math.min(now - base, Math.max(size2-min2, 0));
				side2.style[horw?'width':'height'] = (size2-change)+'px';
				side1.style[horw?'width':'height'] = (size1+change)+'px';
			}
			
			// DWC special (since Yevent is not present)
			if (change) {
				Yfit.makeFit(document.body);
			}
			
			// don't modify handler's left/top, its position is already changed by the above
			return null;
		}

		handler.onDragEnd = function(event) {
			handler.beforeDragMove = handler.onDragEnd = null;
			p.style.cursor = 'auto';
		}
		
		p.style.cursor = horw ? 'e-resize' : 'n-resize';
		
		return startDragging(event, handler);
	}

	// export functions
	self.startDragging = startDragging;
	self.startResizingHeight = startResizingHeight;
	self.startResizingWidth = startResizingWidth;
	return self;
})();
